1 /*
2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021
3 License:   [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License].
4 Authors: Marcelo S. N. Mancini
5 
6 	Copyright Marcelo S. N. Mancini 2018 - 2021.
7 Distributed under the CC BY-4.0 License.
8    (See accompanying file LICENSE.txt or copy at
9 	https://creativecommons.org/licenses/by/4.0/
10 */
11 module hip.graphics.g2d.spritebatch;
12 import hip.graphics.mesh;
13 import hip.graphics.orthocamera;
14 import hip.hiprenderer.renderer;
15 import hip.assets.texture;
16 import hip.hiprenderer.framebuffer;
17 import hip.error.handler;
18 import hip.hiprenderer.shader;
19 import hip.config.renderer;
20 public import hip.api.graphics.batch;
21 public import hip.api.graphics.color;
22 public import hip.api.renderer.shaders.spritebatch;
23 
24 
25 /**
26 *   The spritebatch contains 2 shaders.
27 *   One shader is entirely internal, which you don't have any control, this is for actually being able
28 *   to draw stuff on the screen.
29 *
30 *   The another one is a post processing shader, which the spritebatch doesn't uses by default. If
31 *   setPostProcessingShader()
32 */
33 class HipSpriteBatch : IHipBatch
34 {
35     index_t maxQuads;
36     index_t[] indices;
37     HipSpriteVertex[] vertices;
38 
39     protected bool hasInitTextureSlots;
40     Shader spriteBatchShader;
41 
42     ///Post Processing Shader
43     protected Shader ppShader;
44     protected HipFrameBuffer fb;
45     protected HipTextureRegion fbTexRegion;
46     protected float managedDepth = 0;
47 
48     HipOrthoCamera camera;
49     Mesh mesh;
50 
51     protected IHipTexture[] currentTextures;
52     int usingTexturesCount;
53 
54     uint lastDrawQuadsCount = 0;
55     uint quadsCount;
56 
57 
58     ShaderVar* uMVP, uTex;
59 
60 
61     this(HipOrthoCamera camera = null, index_t maxQuads = DefaultMaxSpritesPerBatch)
62     {
63         import hip.hiprenderer.initializer;
64         import hip.util.conv:to;
65         ErrorHandler.assertLazyExit(index_t.max > maxQuads * 6, "Invalid max quads. Max is "~to!string(index_t.max/6));
66         this.maxQuads = maxQuads;
67         vertices = new HipSpriteVertex[maxQuads*4]; //XYZ -> 3, RGBA -> 4, ST -> 2, TexID 3+4+2+1=10
68         currentTextures = new IHipTexture[](HipRenderer.getMaxSupportedShaderTextures());
69         usingTexturesCount = 0;
70 
71 
72         this.spriteBatchShader = newShader(HipShaderPresets.SPRITE_BATCH);
73         spriteBatchShader.addVarLayout(ShaderVariablesLayout.from!(HipSpriteVertexUniform)(HipRenderer.getInfo));
74         spriteBatchShader.addVarLayout(ShaderVariablesLayout.from!(HipSpriteFragmentUniform)(HipRenderer.getInfo));
75         spriteBatchShader.setBlending(HipBlendFunction.SRC_ALPHA, HipBlendFunction.ONE_MINUS_SRC_ALPHA, HipBlendEquation.ADD);
76 
77         mesh = new Mesh(HipVertexArrayObject.getVAO!HipSpriteVertex, spriteBatchShader);
78         mesh.createVertexBuffer(cast(index_t)(maxQuads*HipSpriteVertex.quadCount), HipResourceUsage.Dynamic);
79         mesh.setIndices(HipRenderer.getQuadIndexBuffer(maxQuads));
80 
81         spriteBatchShader.useLayout.Cbuf;
82         // spriteBatchShader.bind();
83         // spriteBatchShader.sendVars();
84         mesh.sendAttributes();
85 
86 
87         spriteBatchShader.useLayout.Cbuf;
88         spriteBatchShader.bind();
89         spriteBatchShader.sendVars();
90 
91         uMVP = mesh.shader.get("Cbuf1.uMVP", ShaderTypes.vertex);
92         // uTex = mesh.shader.get("Cbuf.uTex", ShaderTypes.fragment);
93 
94         if(camera is null)
95             camera = new HipOrthoCamera();
96         this.camera = camera;
97         mesh.setVertices(vertices);
98         // vertices = cast(HipSpriteVertex[])mesh.vao.VBO.getBuffer();
99         setTexture(HipTexture.getPixelTexture());
100 
101     }
102     void setCurrentDepth(float depth){managedDepth = depth;}
103 
104     void setShader(Shader s)
105     {
106         if(fb is null)
107         {
108             Viewport v = HipRenderer.getCurrentViewport;
109             fb = HipRenderer.newFrameBuffer(cast(int)v.width, cast(int)v.height);
110             // fbTexRegion = new HipTextureRegion(fb.getTexture());
111         }
112         this.ppShader = s;
113     }
114 
115     /**
116     *   Sets the texture slot/index for the current quad and points it to the next quad
117     */
118     void addQuad(void[] quad, int slot)
119     {
120         if(quadsCount+1 > maxQuads)
121             flush();
122 
123         size_t start = quadsCount*4;
124         import core.stdc.string;
125         HipSpriteVertex* v = cast(HipSpriteVertex*)vertices.ptr + start;
126         memcpy(v, quad.ptr, HipSpriteVertex.sizeof * 4);
127         setTID(v[0..4], slot);
128 
129         quadsCount++;
130     }
131 
132     void addQuads(void[] quadsVertices, int slot)
133     {
134         import hip.util.array:swapAt;
135         assert(quadsVertices.length % (HipSpriteVertex.sizeof*4) == 0, "Count must be divisible by HipSpriteVertex.sizeof*4");
136         HipSpriteVertex[] v = cast(HipSpriteVertex[])quadsVertices;
137         uint countOfQuads = cast(uint)(v.length / 4);
138 
139         while(countOfQuads > 0)
140         {
141             size_t remainingQuads = this.maxQuads - this.quadsCount;
142             if(remainingQuads == 0)
143             {
144                 flush();
145                 this.usingTexturesCount = 1;
146                 swapAt(this.currentTextures, 0, slot);//Guarantee the target slot is being used
147                 remainingQuads = this.maxQuads;
148             }
149             size_t quadsToDraw = (countOfQuads < remainingQuads) ? countOfQuads : remainingQuads;
150 
151 
152             size_t start = quadsCount*4;
153             size_t end = start + quadsToDraw*4;
154 
155 
156             vertices[start..end] = v[0..quadsToDraw*4];
157             static if(!GLMaxOneBoundTexture)
158             {
159                 for(int i = 0; i < quadsToDraw; i++)
160                     setTID(vertices[start+i*4..$], slot);
161 
162             }
163 
164             v = v[quadsToDraw*4..$];
165 
166             this.quadsCount+= quadsToDraw;
167 
168 
169             countOfQuads-= quadsToDraw;
170         }
171     }
172 
173     private int getNextTextureID(IHipTexture t)
174     {
175         for(int i = 0; i < usingTexturesCount; i++)
176             if(currentTextures[i] is t)
177                 return i;
178         if(usingTexturesCount < currentTextures.length)
179         {
180             currentTextures[usingTexturesCount] = t;
181             return usingTexturesCount++;
182         }
183         return -1;
184     }
185     /**
186     *   Sets the current texture in use on the sprite batch and returns its slot.
187     */
188     protected int setTexture (IHipTexture texture)
189     {
190         int slot = getNextTextureID(texture);
191         if(slot == -1)
192         {
193             flush();
194             slot = getNextTextureID(texture);
195         }
196         return slot;
197     }
198     protected int setTexture(IHipTextureRegion reg){return setTexture(reg.getTexture());}
199 
200     protected static bool isZeroAlpha(void[] vertices)
201     {
202         HipSpriteVertex[] v = cast(HipSpriteVertex[])vertices;
203         return v[0].vColor.a == 0 && v[1].vColor.a == 0 && v[2].vColor.a == 0 && v[3].vColor.a == 0;
204     }
205 
206     void draw(IHipTexture t, ubyte[] vertices)
207     {
208         if(isZeroAlpha(vertices)) return;
209         ErrorHandler.assertExit(t.getWidth != 0 && t.getHeight != 0, "Tried to draw 0 bounds sprite");
210         int slot = setTexture(t);
211         ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase");
212 
213         if(vertices.length == HipSpriteVertex.sizeof * 4)
214             addQuad(vertices, slot);
215         else
216             addQuads(vertices, slot);
217     }
218 
219     void draw(IHipTexture texture, int x, int y, int z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
220     {
221         import hip.global.gamedef;
222         if(color.a == 0) return;
223         if(quadsCount+1 > maxQuads)
224             flush();
225         if(texture is null)
226             texture = cast()getDefaultTexture();
227 
228         int width = texture.getWidth(), height = texture.getHeight();
229         ErrorHandler.assertExit(width != 0 && height != 0, "Tried to draw 0 bounds texture");
230         int slot = setTexture(texture);
231         ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase");
232 
233         size_t startVertex = quadsCount *4;
234         size_t endVertex = startVertex + 4;
235 
236         getTextureVertices(vertices[startVertex..endVertex], slot, width, height, x,y,managedDepth,color, scaleX, scaleY, rotation);
237         quadsCount++;
238     }
239 
240 
241     void draw(IHipTextureRegion reg, int x, int y, int z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
242     {
243         if(color.a == 0) return;
244         if(quadsCount+1 > maxQuads)
245             flush();
246         ErrorHandler.assertExit(reg.getWidth() != 0 && reg.getHeight() != 0, "Tried to draw 0 bounds region");
247         int slot = setTexture(reg);
248         ErrorHandler.assertExit(slot != -1, "HipTexture slot can't be -1 on draw phase");
249         size_t startVertex = quadsCount*4;
250         size_t endVertex = startVertex + 4;
251 
252         getTextureRegionVertices(vertices[startVertex..endVertex], slot, reg,x,y,managedDepth,color, scaleX, scaleY, rotation);
253         quadsCount++;
254     }
255 
256 
257     private static void setTID(HipSpriteVertex[] vertices, int tid)
258     {
259         static if(!GLMaxOneBoundTexture)
260         {
261             vertices[0].vTexID = tid;
262             vertices[1].vTexID = tid;
263             vertices[2].vTexID = tid;
264             vertices[3].vTexID = tid;
265         }
266     }
267 
268     pragma(inline, true)
269     private static Vector3[4] getBounds(float x, float y, float z, float width, float height, float scaleX = 1, float scaleY = 1)
270     {
271         width*= scaleX;
272         height*= scaleY;
273         return [
274             Vector3(x, y, z),
275             Vector3(x+width, y, z),
276             Vector3(x+width, y+height, z),
277             Vector3(x, y+height, z),
278         ];
279     }
280 
281     pragma(inline, true)
282     private static Vector3[4] getBoundsFromRotation(float x, float y, float z, float width, float height, float rotation, float scaleX = 1, float scaleY = 1)
283     {
284         import hip.math.utils:cos,sin;
285         width*= scaleX;
286         height*= scaleY;
287         float centerX = -width/2;
288         float centerY = -height/2;
289         float x2 = x + width;
290         float y2 = y + height;
291         float c = cos(rotation);
292         float s = sin(rotation);
293 
294         return [
295             Vector3(c*centerX - s*centerY + x, c*centerY + s*centerX + y, z),
296             Vector3(c*x2 - s*centerY + x, c*centerY + s*x2 + y, z),
297             Vector3(c*x2 - s*y2 + x, c*y2 + s*x2 + y, z),
298             Vector3(c*centerX - s*y2 + x, c*y2 + s*centerX + y, z),
299         ];
300     }
301 
302     static void getTextureVertices(HipSpriteVertex[] output, int slot, int width, int height,
303     int x, int y, float z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
304     {
305         Vector3[4] spritePos = void;
306         if(rotation == 0)
307             spritePos = getBounds(x,y,z,width,height,scaleX,scaleY);
308         else
309             spritePos = getBoundsFromRotation(x,y,z,width,height,rotation,scaleX,scaleY);
310 
311 
312         for(size_t i = 0; i < 4; i++)
313         {
314             output[i].vTexST = HipTextureRegion.defaultVerticesV[i];
315             output[i].vColor = color;
316             output[i].vPosition = spritePos[i];
317             static if(!GLMaxOneBoundTexture)
318                 output[i].vTexID = slot;
319         }
320     }
321 
322     static void getTextureRegionVertices(HipSpriteVertex[] output, int slot, IHipTextureRegion reg,
323     int x, int y, float z = 0, in HipColor color = HipColor.white, float scaleX = 1, float scaleY = 1, float rotation = 0)
324     {
325         int width = reg.getWidth();
326         int height = reg.getHeight();
327         float[8] uvVertices = reg.getVertices();
328 
329         Vector3[4] spritePos = rotation == 0 ? getBounds(x,y,z,width,height,scaleX,scaleY) :getBoundsFromRotation(x,y,z,width,height,rotation,scaleX,scaleY);
330 
331         for(size_t i = 0; i < 4; i++)
332         {
333             output[i].vTexST = Vector2(uvVertices[i*2], uvVertices[i*2+1]);
334             output[i].vColor = color;
335             output[i].vPosition = spritePos[i];
336             static if(!GLMaxOneBoundTexture)
337                 output[i].vTexID = slot;
338         }
339     }
340 
341 
342 
343     void draw()
344     {
345 
346         if(quadsCount - lastDrawQuadsCount != 0)
347         {
348             for(int i = usingTexturesCount; i < currentTextures.length; i++)
349                 currentTextures[i] = currentTextures[0];
350             mesh.bind();
351 
352             uMVP.set(camera.getMVP(), true);
353 
354             // mesh.shader.setFragmentVar(uTex, currentTextures);
355             mesh.shader.bindArrayOfTextures(currentTextures, "uTex");
356             mesh.shader.sendVars();
357 
358             size_t start = lastDrawQuadsCount*4;
359             size_t end = quadsCount*4;
360             mesh.updateVertices(cast(void[])vertices[start..end],cast(int)start);
361 
362             // mesh.vao.VBO.unmapBuffer();
363             mesh.draw((quadsCount-lastDrawQuadsCount)*6, HipRendererMode.triangles, lastDrawQuadsCount*6);
364 
365             ///Some operations may require texture unbinding(D3D11 Framebuffer)
366             foreach(i; 0..usingTexturesCount)
367                 currentTextures[i].unbind(i);
368             mesh.unbind();
369         // mesh.vao.VBO.getBuffer();
370 
371         }
372         lastDrawQuadsCount = quadsCount;
373     }
374 
375     void flush()
376     {
377         if(ppShader !is null)
378             fb.bind();
379         draw();
380         lastDrawQuadsCount = quadsCount = usingTexturesCount = 0;
381         if(ppShader !is null)
382         {
383             fb.unbind();
384             draw(fbTexRegion, 0,0 );
385             draw();
386         }
387         lastDrawQuadsCount = quadsCount = usingTexturesCount = 0;
388     }
389 }